/*
 * OpenParts
 * A dynamic-loading components framework for GWT
 * Copyright (C) 2011 Christophe Bouthier  [chris{AT}binary-gastronome{DOT}fr]
 *
 * This work is partially based on work I have done at INRIA (http://www.inria.fr) 
 * in the context of the Qualipso European Project (http://qualipso.org/),
 * The work done at INRIA is Copyright (C) 2006-2011 INRIA
 *
 * This work is distributed under the LGPL version 3
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3
 * as published by the Free Software Foundation. See the GNU
 * Lesser General Public License in LGPL.txt for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package fr.openparts.OpenParts.rebind;

import java.io.PrintWriter;


import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import fr.openparts.OpenParts.client.OPBinder;

/**
 * Generator for implementation of {@link fr.openparts.OpenParts.client.OPBinder}. Create a OPBinder with all necessary methods for binding,
 * thanks to annotations.
 * 
 * @author Christophe Bouthier [chris{AT}binary-gastronome{DOT}fr]
 * @creation.date 18 December 2009
 */
public class OPBinderGenerator extends Generator {

    /**
     * Called by the GWT Compiler when it match the specific rules as defined in OpenParts.gwt.xml. None of the passed parameters should be null.
     * 
     * @see com.google.gwt.core.ext.Generator#generate(com.google.gwt.core.ext.TreeLogger, com.google.gwt.core.ext.GeneratorContext, java.lang.String)
     */
    @Override
    public String generate(TreeLogger logger, GeneratorContext context, String binderName) throws UnableToCompleteException {
        assert logger != null : "parameter logger shouldn't be null";
        assert context != null : "parameter context shouldn't be null";
        assert binderName != null : "parameter binderName shouldn't be null";

        final TypeOracle oracle = context.getTypeOracle();
        assert oracle != null : "TypeOracle cannot be null";

        final JClassType binder = getBinder(binderName, oracle, logger);
        assert binder != null : "binder type cannot be null";

        final JClassType part = getPart(binder, oracle, logger);
        assert part != null : "part type cannot be null";

        final SourceWriter sourceWriter = getSourceWriterForGeneratedBinder(binder, part, context, logger);

        if (sourceWriter != null) {
            final OPBinderWriter binderWriter = new OPBinderWriter(sourceWriter, part, getGeneratedBinderName(binder),
                    getGeneratedBinderFullQualifiedName(binder), logger);
            binderWriter.writeBinder();
        }

        return getGeneratedBinderFullQualifiedName(binder);
    }

    /**
     * Return the JClassType corresponding to the binder interface passed to GWT.create. If the type cannot be found in the classpath by the oracle, an
     * UnableToCompleteException is thrown. Cannot return null.
     * 
     * @param binderName
     *            the name of the binder interface passed to GWT.create. Should be an interface inheriting from {@link OPBinder}. Shouldn't be null.
     * @param oracle
     *            the TypeOracle. Shouldn't be null.
     * @param logger
     *            the logger. Shouldn't be null.
     * @return the JClassType corresponding to the binder interface. Cannot be null.
     * @throws UnableToCompleteException
     *             if the part class cannot be found by the oracle
     */
    protected JClassType getBinder(final String binderName, final TypeOracle oracle, final TreeLogger logger) throws UnableToCompleteException {
        assert binderName != null : "parameter binderName shouldn't be null";
        assert oracle != null : "parameter oracle shouldn't be null";
        assert logger != null : "parameter logger shouldn't be null";

        try {
            final JClassType binderType = oracle.getType(binderName);
            assert binderType != null : "binder type cannot be null";
            return binderType;
        } catch (NotFoundException e) {
            logger.log(TreeLogger.ERROR, "The class passed as parameter of GWT.create (" + binderName + ") cannot be found in the classpath.");
            throw new UnableToCompleteException();
        }
    }

    /**
     * Return the JClassType corresponding to the part class that should be binded. The part class is found as the parameterized type passed to OPBinder. If
     * this type cannot be found in the classpath by the oracle, an UnableToCompleteException is thrown.
     * 
     * @param binder
     *            the binder interface that will bind this part. Shouldn't be null.
     * @param oracle
     *            the TypeOracle. Shouldn't be null.
     * @param logger
     *            the logger. Shouldn't be null.
     * @return the JClassType corresponding to the part to bind. Cannot be null.
     * @throws UnableToCompleteException
     *             if the part class cannot be found by the oracle
     */
    protected JClassType getPart(final JClassType binder, final TypeOracle oracle, final TreeLogger logger) throws UnableToCompleteException {
        assert binder != null : "parameter binder shouldn't be null";
        assert oracle != null : "parameter oracle shouldn't be null";
        assert logger != null : "parameter logger shouldn't be null";

        final JClassType[] interfaces = binder.getImplementedInterfaces();
        assert interfaces != null  : "binder interfaces cannot be null";
        assert interfaces.length > 0 : "binder interfaces cannot be empty";

        String partName = null;
        for (JClassType interfaceType : interfaces) {
            if (interfaceType instanceof JParameterizedType) {
                final JParameterizedType interfaceParam = (JParameterizedType) interfaceType;
                if ("fr.openparts.OpenParts.client.OPBinder".equals(interfaceParam.getQualifiedSourceName())) {
                    // ok, we found the good interface
                    final JClassType[] types = interfaceParam.getTypeArgs();
                    assert types != null  : "part interfaces cannot be null";
                    assert types.length > 0  : "part interfaces cannot be empty";

                    partName = types[0].getParameterizedQualifiedSourceName();
                }
            }
        }

        assert partName != null : "part name cannot be null";

        final JClassType partType;
        try {
            partType = oracle.getType(partName);
        } catch (NotFoundException e) {
            logger.log(TreeLogger.ERROR, "The class passed as parameter of OPBinder (" + partName + ") cannot be found in the classpath.");
            throw new UnableToCompleteException();
        }
        assert partType != null : "part type cannot be null";

        return partType;
    }

    /**
     * Return the name of the package that will contains the generated binder class class. The generated binder package is the same that the package of the
     * binder interface.
     * 
     * @param binder
     *            the binder interface that will be implemented by the generated binder. Shouldn't be null.
     * @return the name of the package of the generated binder class. Cannot be null.
     */
    protected String getGeneratedBinderPackage(final JClassType binder) {
        assert binder != null : "parameter binder shouldn't be null";

        final String generatedBinderPackage = binder.getPackage().getName();
        assert generatedBinderPackage != null : "generated binder package cannot be null";
        return generatedBinderPackage;
    }

    /**
     * Return the name of the generated binder class. The generated binder class name is build from the name of the binder interface.
     * 
     * @param binder
     *            the binder interface that will be implemented by the generated binder. Shouldn't be null.
     * @return the name of the generated binder class. Cannot be null.
     */
    protected String getGeneratedBinderName(final JClassType binder) {
        assert binder != null : "parameter binder shouldn't be null";

        final String generatedBinderName = binder.getName().replace('.', '_') + "Impl";
        assert generatedBinderName != null : "generated binder name cannot be null";
        return generatedBinderName;
    }

    /**
     * Return the fully qualified name of the generated binder class. The generated binder fully qualified name is build from the name of the binder interface.
     * 
     * @param binder
     *            the binder interface that will be implemented by the generated binder. Shouldn't be null.
     * @return the fully qualified name of the generated binder class. Cannot be null.
     */
    protected String getGeneratedBinderFullQualifiedName(final JClassType binder) {
        assert binder != null : "parameter binder shouldn't be null";

        final String generatedBinderPackage = getGeneratedBinderPackage(binder);
        final String generatedBinderName = getGeneratedBinderName(binder);
        assert generatedBinderPackage != null : "generated binder package cannot be null";
        assert generatedBinderName != null : "generated binder name cannot be null";

        final String generatedBinderFullQualifiedName = generatedBinderPackage + "." + generatedBinderName;
        assert generatedBinderFullQualifiedName != null : "generated binder fully qualified name cannot be null";
        return generatedBinderFullQualifiedName;
    }

    /**
     * Create the class factory for the generated binder. Implements the binder interface. Add also the necessary import (especially if the part is external to
     * the binder).
     * 
     * @param binder
     *            the binder interface implemented by the generated binder. Shouldn't be null.
     * @param part
     *            the part to bind. Shouldn't be null.
     * @return the class factory for the generated binder class. Cannot be null.
     */
    protected ClassSourceFileComposerFactory getClassFactoryForGeneratedBinder(final JClassType binder, final JClassType part) {
        assert binder != null : "parameter binder shouldn't be null";;
        assert part != null : "parameter part shouldn't be null";;

        final String generatedBinderPackage = getGeneratedBinderPackage(binder);
        final String generatedBinderName = getGeneratedBinderName(binder);
        assert generatedBinderPackage != null : "generated binder package cannot be null";
        assert generatedBinderName != null : "generated binder name cannot be null";

        final ClassSourceFileComposerFactory classFactory = new ClassSourceFileComposerFactory(generatedBinderPackage, generatedBinderName);
        assert classFactory != null : "class factory cannot be null";

        classFactory.addImplementedInterface(binder.getName());

        classFactory.addImport(binder.getParameterizedQualifiedSourceName());
        classFactory.addImport(part.getParameterizedQualifiedSourceName());

        classFactory.addImport("fr.openparts.OpenParts.client.*");
        classFactory.addImport("com.google.gwt.user.client.*");
        classFactory.addImport("com.google.gwt.user.client.ui.*");

        return classFactory;
    }

    /**
     * Create the source writer that will write the generated binder class. Returns null if the binder has already be generated.
     * 
     * @param binder
     *            the binder interface implemented by the generated binder. Shouldn't be null.
     * @param part
     *            the part to bind. Shouldn't be null.
     * @param context
     *            the context for the generation. Shouldn't be null.
     * @param logger
     *            the logger. Shouldn't be null.
     * @return the source writer that will write the generated binder class. Can be null.
     * @throws UnableToCompleteException
     *             if it is impossible to create a writer for the generated binder class
     */
    protected SourceWriter getSourceWriterForGeneratedBinder(final JClassType binder, final JClassType part, final GeneratorContext context,
            final TreeLogger logger) throws UnableToCompleteException {
        assert binder != null : "parameter binder shouldn't be null";;
        assert part != null : "parameter part shouldn't be null";;
        assert context != null : "parameter context shouldn't be null";;
        assert logger != null : "parameter logger shouldn't be null";;

        final String generatedBinderPackage = getGeneratedBinderPackage(binder);
        final String generatedBinderName = getGeneratedBinderName(binder);
        assert generatedBinderPackage != null : "generated binder package cannot be null";
        assert generatedBinderName != null : "generated binder name cannot be null";

        final ClassSourceFileComposerFactory classFactory = getClassFactoryForGeneratedBinder(binder, part);
        assert classFactory != null : "class factory cannot be null";

        logger.log(TreeLogger.DEBUG, "Trying to create a printWriter for " + getGeneratedBinderFullQualifiedName(binder) + ".");
        final PrintWriter printWriter = context.tryCreate(logger, generatedBinderPackage, generatedBinderName);
        if (printWriter == null) {
            // the type already exist, as it doesn't change from permutation to permutation.
            logger.log(TreeLogger.DEBUG, "Unable to create a printWriter for " + getGeneratedBinderFullQualifiedName(binder) + ".");
            return null;
        }

        final SourceWriter generatedBinderSourceWriter = classFactory.createSourceWriter(context, printWriter);
        if (generatedBinderSourceWriter == null) {
            logger.log(TreeLogger.ERROR, "Unable to create a source writer for " + getGeneratedBinderFullQualifiedName(binder) + ".");
            throw new UnableToCompleteException();
        }

        return generatedBinderSourceWriter;
    }

}
